/*
 * Copyright 2007-2008 Andrew O'Malley
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */


package ws.quokka.plugin.jetty;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.input.InputRequest;
import org.apache.tools.ant.types.Path;
import org.apache.tools.ant.types.ResourceCollection;
import org.apache.tools.ant.types.resources.FileResource;

import org.mortbay.jetty.Handler;
import org.mortbay.jetty.Server;
import org.mortbay.jetty.handler.ContextHandlerCollection;
import org.mortbay.jetty.handler.DefaultHandler;
import org.mortbay.jetty.handler.HandlerCollection;
import org.mortbay.jetty.handler.RequestLogHandler;
import org.mortbay.jetty.nio.SelectChannelConnector;

import org.mortbay.resource.Resource;

import org.mortbay.util.Scanner;

import org.mortbay.xml.XmlConfiguration;

import ws.quokka.core.bootstrap_util.ProjectLogger;
import ws.quokka.core.plugin_spi.support.Setter;
import ws.quokka.core.util.InterruptableInputHandler;

import ws.quokka.plugin.lifecycle.LifeCycle;
import ws.quokka.plugin.lifecycle.LifeCycleImpl;

import java.io.File;
import java.io.IOException;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;


/**
 * JettyPlugin is based on the Maven Jetty plugin - in fact, some portions have been copied and pasted
 * directly (particularly the annotations parsing in PlusConfiguration). However, the elaborate class hierachy
 * has been removed in favour of introducing a "mode" for the few cases where the details differ for "in place",
 * war and war-exploded.
 * I believe this offers the same functionality as the Maven equivalent with the exception of
 * allowing the configuration of connectors, security realms and loggers - they can be configured
 * through the jetty configuration xml file if required by the user.
 * <p/>
 * Unlike the Maven plugin it does not require the jetty plus jars at runtime - they are optional
 */
public class JettyPlugin extends LifeCycleImpl {
    //~ Static fields/initializers -------------------------------------------------------------------------------------

    private static final int IN_PLACE = 0;
    private static final int WAR = 1;
    private static final int WAR_EXPLODED = 2;

    //~ Instance fields ------------------------------------------------------------------------------------------------

    private Server server;
    private int mode;

    //~ Methods --------------------------------------------------------------------------------------------------------

    public void runTemplate() throws Exception {
        mode = IN_PLACE;
        run();
    }

    public void runExplodedTemplate() throws Exception {
        mode = WAR_EXPLODED;
        run();
    }

    public void runWarTemplate() throws Exception {
        mode = WAR;
        run();
    }

    public void stopTemplate() throws InterruptedException {
//        Thread.sleep(500); // Give any executing requests a chance to finish
        Monitor monitor = createMonitor();

        if (monitor != null) {
            monitor.stop();
        }
    }

    public void run() throws Exception {
        createServerInstance();
        setSystemProperties();
        applyJettyConfig();
        addDefaultConnector();

        PluginWebAppContext appContext = configureWebAppContext();
        configureHandlers(appContext);
        start(appContext);
    }

    private void start(PluginWebAppContext appContext)
            throws Exception {
        // start Jetty
        server.start();

        log().info("Started Jetty Server");

        // Start the scanner thread (if necessary) on the main webapp
        String reload = properties().getString("reload");
        assertTrue(reload.equals("manual") || reload.equals("auto"), "Valid values for reload are 'manual' or 'auto'");

        // Keep the thread going if not in daemon mode
        boolean daemon = properties().getBoolean("daemon", false);

        // Set up shutdown listener
        Monitor monitor = createMonitor();

        if (reload.equals("manual") && !daemon) {
            // Reload whenever the user hits ENTER
            log().info("Reloading of the web app is set to manual");

            InputRequest request = new InputRequest("Press ENTER to reload the web app, or 'exit' to terminate");
            InterruptableInputHandler inputHandler = new InterruptableInputHandler(getProject().getDefaultInputStream());
            monitor.monitor(server, inputHandler);

            while (true) {
                try {
                    inputHandler.handleInput(request);
                } catch (BuildException e) {
                    if (e.getCause() instanceof InterruptedException) {
                        return; // Interrupted by a stop request
                    }

                    throw e;
                }

                if ("exit".equals(request.getInput())) {
                    server.stop();

                    return;
                }

                restart(appContext);
            }
        } else {
            monitor.monitor(server, null);

            // Reload when changes are detected
            startScanner(appContext);
        }

        if (!daemon) {
            server.join();
            log().info("Jetty server exiting.");
        }
    }

    private void addDefaultConnector() {
        if (server.getConnectors() == null) { // Not supplied in xml config

            SelectChannelConnector connector = new SelectChannelConnector();
            connector.setPort(properties().getInt("port", 8080));
            connector.setMaxIdleTime(properties().getInt("maxIdle", 30000));
            server.addConnector(connector);
        }
    }

    private void applyJettyConfig() throws Exception {
        // Apply XML configuration ... some of these setting may be overwritten below
        File jettyConfig = properties().getFile("jettyConfig", null);

        if (jettyConfig != null) {
            XmlConfiguration xmlConfiguration = new XmlConfiguration(jettyConfig.toURI().toURL());
            xmlConfiguration.configure(server);
        }
    }

    private void createServerInstance() {
        server = new Server();
        server.setStopAtShutdown(true);
        Resource.setDefaultUseCaches(false); // Prevents caching of URLs
    }

    private void setSystemProperties() {
        Map systemProperties = properties().getMap("sysProp", false, String.class);

        for (Iterator i = systemProperties.entrySet().iterator(); i.hasNext();) {
            Map.Entry entry = (Map.Entry)i.next();
            String key = (String)entry.getKey();

            if (System.getProperty(key) == null) {
                System.setProperty(key, (String)entry.getValue()); // Only set if currently unset
            }
        }
    }

    private PluginWebAppContext configureWebAppContext()
            throws IOException {
        PluginWebAppContext appContext = new PluginWebAppContext();
        String contextPath = properties().getString("contextPath");
        appContext.setContextPath(contextPath.startsWith("/") ? contextPath : ("/" + contextPath));

        Setter setter = new Setter(properties());
        setter.set(appContext, new String[] { "defaultsDescriptor", "overrideDescriptor" });

        File tempDirectory = properties().getFile("tempDirectory", null);

        if (tempDirectory != null) {
            utils().mkdir(tempDirectory);
            appContext.setTempDirectory(tempDirectory);
        }

        File webApp = properties().getFile("webApp");
        assertTrue(webApp.exists(), "Webapp source directory does not exist: " + webApp.getPath());
        appContext.setWar(webApp.getCanonicalPath());

        if (mode == IN_PLACE) {
            File webXml = properties().getFile("webXml", new File(new File(webApp, "WEB-INF"), "web.xml"));
            assertTrue(webXml.exists(), "Web xml does not exist: " + webXml.getPath());
            appContext.setWebXmlFile(webXml);

            File jettyEnvXml = properties().getFile("jettyEnvXml", null);
            assertTrue((jettyEnvXml == null) || jettyEnvXml.exists(),
                "jetty-env.xml does not exist: " + ((jettyEnvXml == null) ? "" : jettyEnvXml.getPath()));
            appContext.setJettyEnvXmlFile(jettyEnvXml);
            appContext.setClassPathFiles(setUpClassPath());
        }

        appContext.setLog(new ProjectLogger(getProject()));
        appContext.configure();

        return appContext;
    }

    private Monitor createMonitor() {
        int stopPort = properties().getInt("stopPort", 0);
        String stopKey = properties().getString("stopKey", null);

        if ((stopPort > 0) && (stopKey != null)) {
            return new Monitor(stopPort, stopKey, new ProjectLogger(getProject())); // Don't use this task's logger
        }

        return null;
    }

    /**
     * Sets up the class path in a conservative fashion, allowing for JSP/HTML projects with no classes
     */
    private Path setUpClassPath() {
        Path classPath = new Path(getProject());

        if (properties().getBoolean("includeInstrumentedOuput", false)
                && isTargetEnabled(LifeCycle.INSTRUMENT_COMPILED)) {
            classPath.add(new Path(getProject(), getLifeCycle().getInstrumentCompiledOutput().getPath()));
            classPath.add(getLifeCycle().getState().getInstrumentedTestPath());
        }

        if (properties().getBoolean("includeTestCompileOutput") && getLifeCycle().getTestComplieOutput().exists()) {
            classPath.append(utils().toPath(getLifeCycle().getTestComplieOutput()));
        }

        if (getLifeCycle().getComplieOutput().exists()) {
            classPath.append(utils().toPath(getLifeCycle().getComplieOutput()));
        }

        String pathId = properties().getString("libPath", null);

        if (pathId != null) {
            classPath.append(getResources().getProjectPath(pathId));
        }

        return classPath;
    }

    /**
     * Run a scanner thread on the given list of files and directories, calling
     * stop/start on the given list of LifeCycle objects if any of the watched
     * files change.
     */
    private void startScanner(final PluginWebAppContext appContext) {
        // Check if scanning is enabled
        int scanIntervalSeconds = properties().getInt("scanIntervalSeconds", 0);

        if (scanIntervalSeconds <= 0) {
            log().info("Reloading of the web app is set to automatic, but is disabled due to the scanIntervalSeconds <= 0");

            return;
        }

        log().info("Reloading of the web app is set to automatic and will reload when changes are detected");

        Scanner scanner = new Scanner();
        scanner.setReportExistingFilesOnStartup(false);
        scanner.setScanInterval(scanIntervalSeconds);
        scanner.setScanDirs(getScanFiles(appContext));
        scanner.setRecursive(true);
        scanner.addListener(new Scanner.BulkListener() {
                public void filesChanged(List filenames)
                        throws Exception {
                    try {
                        // Maven plugin does reconfigure if the project file has changed, but this won't do
                        // anything in quokka unless the project is restarted - I doubt it works in Maven either
                        log().info("Restarting webapp due to detected file changes ...");
                        restart(appContext);
                    } catch (Exception e) {
                        log().error("Error restarting webapp: " + e.getMessage());
                    }
                }
            });
        log().info("Starting scanner at interval of " + scanIntervalSeconds + " seconds.");
        scanner.start();
    }

    private void restart(PluginWebAppContext appContext)
            throws Exception {
        appContext.stop();
        appContext.start();
        log().info("Restart completed.");
    }

    /**
     * Set up the handler structure to receive a webapp. Also put in a DefaultHandler so we get a nice page
     * than a 404 if we hit the root and the webapp's context isn't at root.
     */
    public void configureHandlers(PluginWebAppContext appContext)
            throws Exception {
        ContextHandlerCollection contexts = (ContextHandlerCollection)server.getChildHandlerByClass(ContextHandlerCollection.class);

        if (contexts == null) {
            contexts = new ContextHandlerCollection();

            HandlerCollection handlers = (HandlerCollection)server.getChildHandlerByClass(HandlerCollection.class);

            if (handlers == null) {
                handlers = new HandlerCollection();
                server.setHandler(handlers);
                handlers.setHandlers(new Handler[] { contexts, new DefaultHandler(), new RequestLogHandler() });
            } else {
                handlers.addHandler(contexts);
            }
        }

        contexts.addHandler(appContext);
    }

    public List getScanFiles(PluginWebAppContext appContext) {
        List files = new ArrayList();

        if (mode == IN_PLACE) {
            files.add(appContext.getWebXmlFile());

            File jettyEnvXml = appContext.getJettyEnvXmlFile();
            jettyEnvXml = (jettyEnvXml != null) ? jettyEnvXml : findJettyWebXml(new File(appContext.getWar(), "WEB-INF"));

            if (jettyEnvXml != null) {
                files.add(jettyEnvXml);
            }

            String[] pathFiles = appContext.getClassPathFiles().list();

            for (int i = 0; i < pathFiles.length; i++) {
                files.add(new File(pathFiles[i]));
            }
        } else if (mode == WAR_EXPLODED) {
            File webInfDir = new File(appContext.getWar(), "WEB-INF");
            files.add(new File(webInfDir, "web.xml"));

            File jettyWebXmlFile = findJettyWebXml(webInfDir);

            if (jettyWebXmlFile != null) {
                files.add(jettyWebXmlFile);
            }

            File jettyEnvXmlFile = new File(webInfDir, "jetty-env.xml");

            if (jettyEnvXmlFile.exists()) {
                files.add(jettyEnvXmlFile);
            }

            files.add(new File(webInfDir, "classes"));
            files.add(new File(webInfDir, "lib"));
        } else {
            files.add(new File(appContext.getWar())); // WAR
        }

        files.addAll(getExtraScanTargets());

        return files;
    }

    private File findJettyWebXml(File webInfDir) {
        String[] files = new String[] { "jetty-web.xml", "web-jetty.xml", "jetty6-web.xml" };

        for (int i = 0; i < files.length; i++) {
            File file = new File(webInfDir, files[i]);

            if (file.exists()) {
                return file;
            }
        }

        return null;
    }

    public Collection getExtraScanTargets() {
        List targets = new ArrayList();
        Map scanTargets = properties().getMap("scanTargets", false, ResourceCollection.class);

        for (Iterator i = scanTargets.values().iterator(); i.hasNext();) {
            ResourceCollection rc = (ResourceCollection)i.next();

            for (Iterator j = rc.iterator(); j.hasNext();) {
                FileResource fileResource = (FileResource)j.next();
                targets.add(fileResource.getFile());
            }
        }

        return targets;
    }
}
